package demoMod.anm2editor.core;

import com.badlogic.gdx.*;
import com.badlogic.gdx.backends.lwjgl.*;
import com.badlogic.gdx.backends.lwjgl.audio.OpenALAudio;
import com.badlogic.gdx.utils.*;
import demoMod.anm2editor.utils.Anm2ApplicationLogger;
import demoMod.anm2editor.utils.ReflectionHacks;
import org.apache.logging.log4j.LogManager;
import org.lwjgl.opengl.Display;

import java.awt.*;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Anm2EditorApplication implements Application {
    protected final LwjglGraphics graphics;
    protected OpenALAudio audio;
    protected final LwjglFiles files;
    protected final LwjglInput input;
    protected final LwjglNet net;
    protected final ApplicationListener listener;
    protected Thread mainLoopThread;
    protected boolean running = true;
    protected final Array<Runnable> runnables = new Array<>();
    protected final Array<Runnable> executedRunnables = new Array<>();
    protected final SnapshotArray<LifecycleListener> lifecycleListeners = new SnapshotArray<>(LifecycleListener.class);
    protected int logLevel = LOG_INFO;
    protected ApplicationLogger applicationLogger;
    protected String preferencesdir;
    protected Files.FileType preferencesFileType;
    protected LwjglApplicationConfiguration config;

    private static LwjglGraphics getLwjglGraphics(LwjglApplicationConfiguration config) {
        try {
            Constructor<LwjglGraphics> constructor = LwjglGraphics.class.getDeclaredConstructor(LwjglApplicationConfiguration.class);
            constructor.setAccessible(true);
            return constructor.newInstance(config);
        } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

    private static LwjglGraphics getLwjglGraphics(Canvas canvas) {
        try {
            Constructor<LwjglGraphics> constructor = LwjglGraphics.class.getDeclaredConstructor(Canvas.class);
            constructor.setAccessible(true);
            return constructor.newInstance(canvas);
        } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

    private static LwjglGraphics getLwjglGraphics(Canvas canvas, LwjglApplicationConfiguration config) {
        try {
            Constructor<LwjglGraphics> constructor = LwjglGraphics.class.getDeclaredConstructor(Canvas.class, LwjglApplicationConfiguration.class);
            constructor.setAccessible(true);
            return constructor.newInstance(canvas, config);
        } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

    public Anm2EditorApplication (ApplicationListener listener, String title, int width, int height) {
        this(listener, createConfig(title, width, height));
    }

    public Anm2EditorApplication (ApplicationListener listener) {
        this(listener, null, 640, 480);
    }

    public Anm2EditorApplication (ApplicationListener listener, LwjglApplicationConfiguration config) {
        this(listener, config, getLwjglGraphics(config));
    }

    public Anm2EditorApplication (ApplicationListener listener, Canvas canvas) {
        this(listener, new LwjglApplicationConfiguration(), getLwjglGraphics(canvas));
    }

    public Anm2EditorApplication (ApplicationListener listener, LwjglApplicationConfiguration config, Canvas canvas) {
        this(listener, config, getLwjglGraphics(canvas, config));
    }

    public Anm2EditorApplication (ApplicationListener listener, LwjglApplicationConfiguration config, LwjglGraphics graphics) {
        LwjglNativesLoader.load();
        setApplicationLogger(new Anm2ApplicationLogger(LogManager.getLogger(Anm2EditorApplication.class)));

        if (config.title == null) config.title = listener.getClass().getSimpleName();
        this.graphics = graphics;
        if (!LwjglApplicationConfiguration.disableAudio) {
            try {
                audio = new OpenALAudio(config.audioDeviceSimultaneousSources, config.audioDeviceBufferCount,
                        config.audioDeviceBufferSize);
            } catch (Throwable t) {
                log("Anm2EditorApplication", "Couldn't initialize audio, disabling audio", t);
                LwjglApplicationConfiguration.disableAudio = true;
            }
        }
        files = new LwjglFiles();
        input = new LwjglInput();
        net = new LwjglNet();
        this.listener = listener;
        this.preferencesdir = config.preferencesDirectory;
        this.preferencesFileType = config.preferencesFileType;
        this.config = config;

        Gdx.app = this;
        Gdx.graphics = graphics;
        Gdx.audio = audio;
        Gdx.files = files;
        Gdx.input = input;
        Gdx.net = net;
        initialize();
    }

    private static LwjglApplicationConfiguration createConfig (String title, int width, int height) {
        LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
        config.title = title;
        config.width = width;
        config.height = height;
        config.vSyncEnabled = true;
        return config;
    }

    private void initialize () {
        mainLoopThread = new Thread("Anm2 Editor Application") {
            @Override
            public void run () {
                graphics.setVSync(Anm2EditorApplication.this.config.vSyncEnabled);
                try {
                    Anm2EditorApplication.this.mainLoop();
                } catch (Throwable t) {
                    if (audio != null) audio.dispose();
                    Gdx.input.setCursorCatched(false);
                    if (t instanceof RuntimeException)
                        throw (RuntimeException)t;
                    else
                        throw new GdxRuntimeException(t);
                }
            }
        };
        mainLoopThread.start();
    }

    void mainLoop () {
        SnapshotArray<LifecycleListener> lifecycleListeners = this.lifecycleListeners;

        try {
            ReflectionHacks.getCachedMethod(LwjglGraphics.class, "setupDisplay").invoke(graphics);
        } catch (Exception e) {
            throw new GdxRuntimeException(e);
        }

        listener.create();
        ReflectionHacks.setPrivate(graphics, LwjglGraphics.class, "resize", true);

        int lastWidth = graphics.getWidth();
        int lastHeight = graphics.getHeight();

        ReflectionHacks.setPrivate(graphics, LwjglGraphics.class, "lastTime", System.nanoTime());
        boolean wasActive = true;
        while (running) {
            Display.processMessages();
            if (Display.isCloseRequested()) exit();

            boolean isActive = Display.isActive();
            if (wasActive && !isActive) { // if it's just recently minimized from active state
                wasActive = false;
                synchronized (lifecycleListeners) {
                    LifecycleListener[] listeners = lifecycleListeners.begin();
                    for (int i = 0, n = lifecycleListeners.size; i < n; ++i)
                        listeners[i].pause();
                    lifecycleListeners.end();
                }
                listener.pause();
            }
            if (!wasActive && isActive) { // if it's just recently focused from minimized state
                wasActive = true;
                synchronized (lifecycleListeners) {
                    LifecycleListener[] listeners = lifecycleListeners.begin();
                    for (int i = 0, n = lifecycleListeners.size; i < n; ++i)
                        listeners[i].resume();
                    lifecycleListeners.end();
                }
                listener.resume();
            }

            boolean shouldRender = false;

            Canvas canvas = ReflectionHacks.getPrivate(graphics, LwjglGraphics.class, "canvas");
            if (canvas != null) {
                int width = canvas.getWidth();
                int height = canvas.getHeight();
                if (lastWidth != width || lastHeight != height) {
                    lastWidth = width;
                    lastHeight = height;
                    Gdx.gl.glViewport(0, 0, lastWidth, lastHeight);
                    listener.resize(lastWidth, lastHeight);
                    shouldRender = true;
                }
            } else {
                this.config.x = Display.getX();
                this.config.y = Display.getY();
                boolean resize = ReflectionHacks.getPrivate(graphics, LwjglGraphics.class, "resize");
                if (resize || Display.wasResized()
                        || (int)(Display.getWidth() * Display.getPixelScaleFactor()) != this.config.width
                        || (int)(Display.getHeight() * Display.getPixelScaleFactor()) != this.config.height) {
                    ReflectionHacks.setPrivate(graphics, LwjglGraphics.class, "resize", false);
                    this.config.width = (int)(Display.getWidth() * Display.getPixelScaleFactor());
                    this.config.height = (int)(Display.getHeight() * Display.getPixelScaleFactor());
                    Gdx.gl.glViewport(0, 0, this.config.width, this.config.height);
                    listener.resize(this.config.width, this.config.height);
                    graphics.requestRendering();
                }
            }

            if (executeRunnables()) shouldRender = true;

            // If one of the runnables set running to false, for example after an exit().
            if (!running) break;

            input.update();
            shouldRender |= graphics.shouldRender();
            try {
                ReflectionHacks.getCachedMethod(LwjglInput.class, "processEvents").invoke(input);
            } catch (Exception e) {
                error("Anm2EditorApplication", "Exception occurred while processing input events!", e);
                MainWindow.processCrash();
            }
            if (audio != null) audio.update();

            if (!isActive && this.config.backgroundFPS == -1) shouldRender = false;
            int frameRate = isActive ? this.config.foregroundFPS : this.config.backgroundFPS;
            if (shouldRender) {
                try {
                    ReflectionHacks.getCachedMethod(LwjglGraphics.class, "updateTime").invoke(graphics);
                    ReflectionHacks.setPrivate(graphics, LwjglGraphics.class, "frameId", graphics.getFrameId() + 1);
                } catch (IllegalAccessException | InvocationTargetException e) {
                    e.printStackTrace();
                }
                listener.render();
                Display.update(false);
            } else {
                // Sleeps to avoid wasting CPU in an empty loop.
                if (frameRate == -1) frameRate = 10;
                if (frameRate == 0) frameRate = this.config.backgroundFPS;
                if (frameRate == 0) frameRate = 30;
            }
            if (frameRate > 0) Display.sync(frameRate);
        }

        synchronized (lifecycleListeners) {
            LifecycleListener[] listeners = lifecycleListeners.begin();
            for (int i = 0, n = lifecycleListeners.size; i < n; ++i) {
                listeners[i].pause();
                listeners[i].dispose();
            }
            lifecycleListeners.end();
        }
        listener.pause();
        listener.dispose();
        Display.destroy();
        if (audio != null) audio.dispose();
        if (this.config.forceExit) System.exit(-1);
    }

    public boolean executeRunnables () {
        synchronized (runnables) {
            for (int i = runnables.size - 1; i >= 0; i--)
                executedRunnables.add(runnables.get(i));
            runnables.clear();
        }
        if (executedRunnables.size == 0) return false;
        do
            executedRunnables.pop().run();
        while (executedRunnables.size > 0);
        return true;
    }

    @Override
    public ApplicationListener getApplicationListener () {
        return listener;
    }

    @Override
    public Audio getAudio () {
        return audio;
    }

    @Override
    public Files getFiles () {
        return files;
    }

    @Override
    public LwjglGraphics getGraphics () {
        return graphics;
    }

    @Override
    public Input getInput () {
        return input;
    }

    @Override
    public Net getNet () {
        return net;
    }

    @Override
    public ApplicationType getType () {
        return ApplicationType.Desktop;
    }

    @Override
    public int getVersion () {
        return 0;
    }

    public void stop () {
        running = false;
        try {
            mainLoopThread.join();
        } catch (Exception ignored) {
        }
    }

    @Override
    public long getJavaHeap () {
        return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
    }

    @Override
    public long getNativeHeap () {
        return getJavaHeap();
    }

    ObjectMap<String, Preferences> preferences = new ObjectMap<>();

    @Override
    public Preferences getPreferences (String name) {
        if (preferences.containsKey(name)) {
            return preferences.get(name);
        } else {
            Preferences prefs = new LwjglPreferences(new LwjglFileHandle(new File(preferencesdir, name), preferencesFileType));
            preferences.put(name, prefs);
            return prefs;
        }
    }

    @Override
    public Clipboard getClipboard () {
        return new LwjglClipboard();
    }

    @Override
    public void postRunnable (Runnable runnable) {
        synchronized (runnables) {
            runnables.add(runnable);
            Gdx.graphics.requestRendering();
        }
    }

    @Override
    public void debug (String tag, String message) {
        if (logLevel >= LOG_DEBUG) getApplicationLogger().debug(tag, message);
    }

    @Override
    public void debug (String tag, String message, Throwable exception) {
        if (logLevel >= LOG_DEBUG) getApplicationLogger().debug(tag, message, exception);
    }

    @Override
    public void log (String tag, String message) {
        if (logLevel >= LOG_INFO) getApplicationLogger().log(tag, message);
    }

    @Override
    public void log (String tag, String message, Throwable exception) {
        if (logLevel >= LOG_INFO) getApplicationLogger().log(tag, message, exception);
    }

    @Override
    public void error (String tag, String message) {
        if (logLevel >= LOG_ERROR) getApplicationLogger().error(tag, message);
    }

    @Override
    public void error (String tag, String message, Throwable exception) {
        if (logLevel >= LOG_ERROR) getApplicationLogger().error(tag, message, exception);
    }

    @Override
    public void setLogLevel (int logLevel) {
        this.logLevel = logLevel;
    }

    @Override
    public int getLogLevel () {
        return logLevel;
    }

    @Override
    public void setApplicationLogger (ApplicationLogger applicationLogger) {
        this.applicationLogger = applicationLogger;
    }

    @Override
    public ApplicationLogger getApplicationLogger () {
        return applicationLogger;
    }


    @Override
    public void exit () {
        postRunnable(() -> running = false);
    }

    @Override
    public void addLifecycleListener (LifecycleListener listener) {
        synchronized (lifecycleListeners) {
            lifecycleListeners.add(listener);
        }
    }

    @Override
    public void removeLifecycleListener (LifecycleListener listener) {
        synchronized (lifecycleListeners) {
            lifecycleListeners.removeValue(listener, true);
        }
    }
}
